跳到主要内容

Java Unsafe类

Unsafe类是什么

参考资料 Java魔法类:Unsafe应用解析

在第一次学习 Java时就发现了 Java没有指针的概念,所以无法直接操作内存,遂一直以为 Java无法操作内存,但实际上 Java可以通过这个 Unsafe类来操作内存

Unsafe 主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险,所以这个 Unsafe类如它名字那样,是不安全的

它拥有以下的功能

f182555953e29cec76497ebaec526fd1297846.png

取得 Unsafe实例

要获取这个 Unsafe 对象有点麻烦,表面上它提供了一个 getUnsafe() 方法让用户能直接取得这个 Unsafe 对象的实例,实际上这个方法需要通过 Java 命令行命令 -Xbootclasspath/a 把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。

那是否没有简单点的办法了呢?可以使用反射取得这个 Unsafe 的内部实例 theUnsafe

private static final Unsafe theUnsafe = new Unsafe();

所以使用反射取得这个对象如下

public class Temp {
public static void main(String[] args) throws Throwable {
Unsafe unsafe = null;

Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 因为这个 theUnsafe 是用的 private 修饰的,所以需要使用 setAccessible 打开
field.setAccessible(true);
// 取得静态对象是用 null
unsafe = (Unsafe) field.get(null);
}
}

内存操作

主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法,可以看到这些方法都是 native

//分配内存, 相当于 C++ 的 malloc 函数
public native long allocateMemory(long bytes);

//扩充内存
public native long reallocateMemory(long address, long bytes);

//释放内存
public native void freeMemory(long address);

//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);

//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);

//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);

//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);

//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);

//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

通常在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存。

与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。

为什么要使用堆外内存?

1、对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。

2、提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

操作对象属性

实际上 Unsafe不存在操作属性一说,它是直接给内存空间赋值

使用例子(下面直接使用了 getObject,实际上八种基本类型都有它专门的方法)

public class Temp {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafeInstance();
// 获取指定属性在内存的偏移量
long nameOffset = unsafe.staticFieldOffset(User.class.getDeclaredField("name"));
// 再取得非静态属性的偏移量
long telephoneOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("telephone"));


// 创建对象,开辟空间
User user = new User();
// 给属性赋值,这种直接赋值是无视访问修饰符的(给指定的内存空间赋值)
unsafe.putObject(user, nameOffset, "alsritter"); //注意,这里无法输入中文,否则会报错
unsafe.putObject(user, telephoneOffset, "0987654321");

// 取出属性值(实际就是取出空间的数据)
String name = unsafe.getObject(user, nameOffset).toString();
String telephone = unsafe.getObject(user, telephoneOffset).toString();

// 打印结果
System.out.println(name + " " + telephone);
}

// 封装一个工具方法
public static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 因为这个 theUnsafe 是用的 private 修饰的,所以需要使用 setAccessible 打开
field.setAccessible(true);
// 取得静态对象是用 null
return (Unsafe) field.get(null);
}

// 创建一个用于操作的类
static class User {
// 静态成员变量
static String name = "张三";// 注意一定要赋初值
// 非静态成员变量
String telephone = "12345678";
}

}

操作数组空间

数组有个特点就是空间连续,然后数据类型一致(数组是不允许使用泛型的)

所以一般就是获取数组的第一个元素的偏移量,以及每个元素之间的偏移增量,通过这两个数据就可以获取数组中的每一个数据

// 获取数组第一个元素的偏移量位置
@ForceInline
public int arrayBaseOffset(Class<?> arrayClass) {
return theInternalUnsafe.arrayBaseOffset(arrayClass);
}

// 获取数组元素之间的偏移量增量
@ForceInline
public int arrayIndexScale(Class<?> arrayClass) {
return theInternalUnsafe.arrayIndexScale(arrayClass);
}

注意:虽然上面两个方法理论上可以获取数组元素的偏移量(上面两个方法使用可能会报错),但是 Unsafe 已经内置了很多常用类型的数组的第一个元素的偏移量和增量

内置的偏移量

/** The value of {@code arrayBaseOffset(boolean[].class)} */
public static final int ARRAY_BOOLEAN_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_BOOLEAN_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(byte[].class)} */
public static final int ARRAY_BYTE_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_BYTE_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(short[].class)} */
public static final int ARRAY_SHORT_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_SHORT_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(char[].class)} */
public static final int ARRAY_CHAR_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_CHAR_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(int[].class)} */
public static final int ARRAY_INT_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_INT_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(long[].class)} */
public static final int ARRAY_LONG_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_LONG_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(float[].class)} */
public static final int ARRAY_FLOAT_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_FLOAT_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(double[].class)} */
public static final int ARRAY_DOUBLE_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_DOUBLE_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(Object[].class)} */
public static final int ARRAY_OBJECT_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_OBJECT_BASE_OFFSET;

内置的增量

/** The value of {@code arrayIndexScale(boolean[].class)} */
public static final int ARRAY_BOOLEAN_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_BOOLEAN_INDEX_SCALE;

/** The value of {@code arrayIndexScale(byte[].class)} */
public static final int ARRAY_BYTE_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_BYTE_INDEX_SCALE;

/** The value of {@code arrayIndexScale(short[].class)} */
public static final int ARRAY_SHORT_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_SHORT_INDEX_SCALE;

/** The value of {@code arrayIndexScale(char[].class)} */
public static final int ARRAY_CHAR_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_CHAR_INDEX_SCALE;

/** The value of {@code arrayIndexScale(int[].class)} */
public static final int ARRAY_INT_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_INT_INDEX_SCALE;

/** The value of {@code arrayIndexScale(long[].class)} */
public static final int ARRAY_LONG_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_LONG_INDEX_SCALE;

/** The value of {@code arrayIndexScale(float[].class)} */
public static final int ARRAY_FLOAT_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_FLOAT_INDEX_SCALE;

/** The value of {@code arrayIndexScale(double[].class)} */
public static final int ARRAY_DOUBLE_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_DOUBLE_INDEX_SCALE;

/** The value of {@code arrayIndexScale(Object[].class)} */
public static final int ARRAY_OBJECT_INDEX_SCALE = jdk.internal.misc.Unsafe.ARRAY_OBJECT_INDEX_SCALE;

使用例子

public class Temp {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafeInstance();
String[] names = {"张三", "李四", "王五", "赵六"};
// 取出第一个元素的数据
Object firstObj = unsafe.getObject(names, Unsafe.ARRAY_OBJECT_BASE_OFFSET);
System.out.println(firstObj);

// 修改第二个元素的值
unsafe.putObject(names,
Unsafe.ARRAY_OBJECT_BASE_OFFSET + // 因为是第二个元素,所以要加一个偏移量
Unsafe.ARRAY_OBJECT_INDEX_SCALE,
"王二狗"
);

System.out.println(names[1]);
}

// 封装一个工具方法
public static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 因为这个 theUnsafe 是用的 private 修饰的,所以需要使用 setAccessible 打开
field.setAccessible(true);
// 取得静态对象是用 null
return (Unsafe) field.get(null);
}
}

线程的挂起和恢复

// 释放被 park 创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的
public native void unpark(Object thread);

// 阻塞当前线程,一直等待 unpark 方法被调用,这里的时间有点坑,看下面的代码
public native void park(boolean isAbsolute, long time);

使用例

public class Temp {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Unsafe unsafe = getUnsafeInstance();
Thread thread = new Thread(() -> {
for (int i = 1; ; i++) { // 死循环
System.out.println("Thread: " + i);
// 如果 i 是 3的倍数则挂起线程
if (i % 3 == 0) {
System.out.println("park!");
// 第一个参数为 true 则以毫秒为单位,false以纳秒为单位
// 第二个参数就是时长(这个时长有点神奇,自己看下面的代码,总之这里代表 10 秒)
unsafe.park(true, (10 * 1000L) + System.currentTimeMillis());
}
}
});

// 启动线程
thread.start();

while (true) {
// 主线程等待 3秒就唤醒子线程
TimeUnit.SECONDS.sleep(3);
unsafe.unpark(thread);
}
}

// 封装一个工具方法
public static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 因为这个 theUnsafe 是用的 private 修饰的,所以需要使用 setAccessible 打开
field.setAccessible(true);
// 取得静态对象是用 null
return (Unsafe) field.get(null);
}
}

CAS 机制

所谓原子操作(atomic operation)是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch)。所以对于原子操作是无需 synchronized

举个例子:

int temp = i + 1;
i = temp;

CPU 可能执行上面的两步执行到一半,这个线程的时间片就到了,这时其它线程也执行这块代码,把数据给改了。所以加上 synchronized 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

当然只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作,上面的例子只是说明 synchronized 如何保证原子性的

public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);